using System;
using System.Collections.Specialized;
using System.Drawing;
using System.Text.RegularExpressions;
using System.Xml;
using Wilco.SyntaxHighlighting.Engine.Node;
using Wilco.SyntaxHighlighting.Engine.Node.Implementation.Xml;
using Wilco.SyntaxHighlighting.Engine.Node.Occurrence;
using Wilco.SyntaxHighlighting.Engine.Scanner;
using Wilco.SyntaxHighlighting.Engine.Tokenizer;

namespace Wilco.SyntaxHighlighting.Engine.Scanner.Implementation
{
    /// <summary>
    /// Represents an Xml scanner.
    /// </summary>
    public class XmlScanner : ScannerBase
    {
        private XmlSpecialCharNode xmlSpecialCharNode;
        private XmlNamespaceNode xmlNamespaceNode;
        private XmlTagNode xmlTagNode;
        private XmlCommentNode xmlCommentNode;
        private XmlAttributeNameNode xmlAttributeNameNode;
        private XmlAttributeValueNode xmlAttributeValueNode;
        private bool identifierEnabled;

        /// <summary>
        /// Occurs when XML was matched.
        /// </summary>
        public event MatchEventHandler Match;

        /// <summary>
        /// Gets or sets the Xml special char node.
        /// </summary>
        public XmlSpecialCharNode XmlSpecialCharNode
        {
            get
            {
                return xmlSpecialCharNode;
            }
            set
            {
                if (value != xmlSpecialCharNode)
                {
                    xmlSpecialCharNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml namespace node.
        /// </summary>
        public XmlNamespaceNode XmlNamespaceNode
        {
            get
            {
                return xmlNamespaceNode;
            }
            set
            {
                if (value != xmlNamespaceNode)
                {
                    xmlNamespaceNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml tag node.
        /// </summary>
        public XmlTagNode XmlTagNode
        {
            get
            {
                return xmlTagNode;
            }
            set
            {
                if (value != xmlTagNode)
                {
                    xmlTagNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml comment node.
        /// </summary>
        public XmlCommentNode XmlCommentNode
        {
            get
            {
                return xmlCommentNode;
            }
            set
            {
                if (value != xmlCommentNode)
                {
                    xmlCommentNode = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Xml attribute name node.
        /// </summary>
        public XmlAttributeNameNode XmlAttributeNameNode
        {
            get
            {
                return xmlAttributeNameNode;
            }
            set
            {
                if (value != xmlAttributeNameNode)
                {
                    xmlAttributeNameNode = value;
                }
            }
        }


        /// <summary>
        /// Gets or sets the Xml attribute value node.
        /// </summary>
        public XmlAttributeValueNode XmlAttributeValueNode
        {
            get
            {
                return xmlAttributeValueNode;
            }
            set
            {
                if (value != xmlAttributeValueNode)
                {
                    xmlAttributeValueNode = value;
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of an <see cref="XmlScanner"/> class.
        /// </summary>
        public XmlScanner() : base(null, null)
        {
            //
        }

        /// <summary>
        /// Initializes a new instance of an <see cref="XmlScanner"/> class.
        /// </summary>
        /// <param name="tokenizer">The <see cref="TokenizerBase"/> which is used to tokenize the source code.</param>
        /// <param name="scannerResult">The <see cref="OccurrenceCollection"/> which will contain the scanner result.</param>
        public XmlScanner(TokenizerBase tokenizer, OccurrenceCollection scannerResult) : base(tokenizer, scannerResult)
        {
            xmlSpecialCharNode = new XmlSpecialCharNode();
            xmlSpecialCharNode.ForeColor = Color.Blue;
            xmlTagNode = new XmlTagNode();
            xmlTagNode.ForeColor = Color.Maroon;
            xmlNamespaceNode = new XmlNamespaceNode();
            xmlNamespaceNode.ForeColor = Color.MediumVioletRed;
            xmlCommentNode = new XmlCommentNode();
            xmlCommentNode.ForeColor = Color.Green;
            xmlAttributeNameNode = new XmlAttributeNameNode();
            xmlAttributeNameNode.ForeColor = Color.Red;
            xmlAttributeValueNode = new XmlAttributeValueNode();
            xmlAttributeValueNode.ForeColor = Color.Blue;
            SetID("XmlScanner");
        }

        /// <summary>
        /// Initializes a new instance of a <see cref="IScanner"/> implementation class.
        /// </summary>
        /// <param name="tokenizer">The <see cref="TokenizerBase"/> which is used to tokenize the source code.</param>
        /// <param name="scannerResult">The <see cref="OccurrenceCollection"/> which will contain the scanner result.</param>
        /// <returns>A new instance of a <see cref="IScanner"/> implementation class.</returns>
        public override IScanner Create(TokenizerBase tokenizer, OccurrenceCollection scannerResult)
        {
            return new XmlScanner(tokenizer, scannerResult);
        }

        /// <summary>
        /// Loads the state of the scanner.
        /// </summary>
        /// <param name="state">An <see cref="System.Object"/> that contains the state of the scanner.</param>
        public override void LoadState(object state)
        {
            XmlElement element = (XmlElement)state;
			
            LoadNode(element, xmlSpecialCharNode, "SpecialCharNode");
            LoadNode(element, xmlTagNode, "TagNode");
            LoadNode(element, xmlNamespaceNode, "NamespaceNode");
            LoadNode(element, xmlCommentNode, "CommentNode");
            LoadNode(element, xmlAttributeNameNode, "AttributeNameNode");
            LoadNode(element, xmlAttributeValueNode, "AttributeValueNode");
        }

        /// <summary>
        /// Loads the information for a node.
        /// </summary>
        /// <param name="element">The <see cref="System.Xml.XmlElement"/> which contains the information about the node.</param>
        /// <param name="node">The <see cref="INode"/> implementation class for which the information will be set.</param>
        /// <param name="name">The name of the node.</param>
        private static void LoadNode(XmlNode element, INode node, string name)
        {
            FontConverter converter = new FontConverter();
            XmlElement nodeElement = (XmlElement)element.SelectSingleNode(String.Format("nodes/node[@name='{0}']", name));
            node.BackColor = ColorTranslator.FromHtml(element.SelectSingleNode("settings/setting[@name='BackColor']").InnerText);
            node.ForeColor = ColorTranslator.FromHtml(element.SelectSingleNode("settings/setting[@name='ForeColor']").InnerText);
            node.Font = (Font)converter.ConvertFromString(element.SelectSingleNode("settings/setting[@name='Font']").InnerText);
            node.NavigateUrl = element.SelectSingleNode("settings/setting[@name='NavigateUrl']").InnerText;
        }

        /// <summary>
        /// Saves the current state of the scanner.
        /// </summary>
        /// <param name="container">The container which will contain the state.</param>
        /// <returns>An <see cref="System.Object"/> that contains the state of the scanner.</returns>
        public override object SaveState(object container)
        {
            XmlDocument document = (XmlDocument)container;
            XmlElement element = (XmlElement)base.SaveState(container);

            // Save settings.
            XmlElement nodeRootElement = document.CreateElement("nodes");
            element.AppendChild(nodeRootElement);

            StoreNode(document, nodeRootElement, xmlSpecialCharNode, "SpecialCharNode");
            StoreNode(document, nodeRootElement, xmlTagNode, "TagNode");
            StoreNode(document, nodeRootElement, xmlNamespaceNode, "NamespaceNode");
            StoreNode(document, nodeRootElement, xmlCommentNode, "CommentNode");
            StoreNode(document, nodeRootElement, xmlAttributeNameNode, "AttributeNameNode");
            StoreNode(document, nodeRootElement, xmlAttributeValueNode, "AttributeValueNode");

            return element;
        }

        /// <summary>
        /// Stores a node.
        /// </summary>
        /// <param name="document">The document which will contain the settings for this node.</param>
        /// <param name="nodeRootElement">The element which will hold the node's settings.</param>
        /// <param name="node">The node which should be represented.</param>
        /// <param name="name">The name of the node.</param>
        private static void StoreNode(XmlDocument document, XmlNode nodeRootElement, INode node, string name)
        {
            XmlElement nodeElement = document.CreateElement("node");
            nodeElement.SetAttribute("name", name);
            nodeRootElement.AppendChild(nodeElement);

            XmlElement settingRootElement = document.CreateElement("settings");
            nodeElement.AppendChild(settingRootElement);

            FontConverter converter = new FontConverter();
            settingRootElement.AppendChild(CreateSetting(document, "BackColor", ColorTranslator.ToHtml(node.BackColor)));
            settingRootElement.AppendChild(CreateSetting(document, "ForeColor", ColorTranslator.ToHtml(node.ForeColor)));
            settingRootElement.AppendChild(CreateSetting(document, "ForeColor", converter.ConvertToString(node.Font)));
            settingRootElement.AppendChild(CreateSetting(document, "NavigateUrl", node.NavigateUrl));
        }

        /// <summary>
        /// Creates a steting.
        /// </summary>
        /// <param name="document">The document which will contain the setting.</param>
        /// <param name="name">The name of the setting.</param>
        /// <param name="value">The value of the setting.</param>
        /// <returns>The <see cref="System.Xml.XmlElement"/> which represents the setting.</returns>
        private static XmlElement CreateSetting(XmlDocument document, string name, string value)
        {
            XmlElement element = document.CreateElement("setting");
            element.SetAttribute("name", name);
            element.InnerText = value;
            return element;
        }

        /// <summary>
        /// Scans a token.
        /// </summary>
        /// <remarks>
        /// An <see cref="IScanner"/> implementation will generally have a reference to a 
        /// <see cref="NodeCollection"/> which will be used to store results of a scan.
        /// </remarks>
        /// <param name="token">A token from the source code.</param>
        public override void Scan(string token)
        {
            if (!Enabled)
            {
                if (Child != null)
                {
                    Child.Scan(token);
                }
            }
            else
            {
                bool isMatch = false;

                // Match the closing tag, such as "</asp:Label>";
                if ((Tokenizer.Position + 2 <= Tokenizer.Source.Length) && Tokenizer.GetNextTokens(2) == "</")
                {
                    ScannerResult.Add(new Occurrence(Tokenizer.Position, 2, xmlSpecialCharNode));
					
                    // Match [special chars/namespace/]tag.
                    int offset = 2;
                    string ns = MatchNamespace(offset);
                    if (ns.Length > 0)
                        offset += ns.Length + 1;

                    string tag = MatchTag(offset);
                    if (tag.Length > 0)
                        offset += tag.Length;

                    // Match end character of the closing tag (">").
                    int endCharIndex = Tokenizer.Source.IndexOfAny("<>".ToCharArray(), Tokenizer.Position + offset);
                    if (endCharIndex > -1)
                    {
                        if (Tokenizer.Source.Substring(endCharIndex, 1).IndexOfAny("<".ToCharArray()) == -1)
                        {
                            ScannerResult.Add(new Occurrence(endCharIndex, 1, xmlSpecialCharNode));
                            OnMatch(new MatchEventArgs(Tokenizer.Position, endCharIndex + 1, ns, tag, null, MatchType.EndTag));
                        }
                        Tokenizer.MoveTo(endCharIndex);
                    }
                    else
                        Tokenizer.MoveTo(Tokenizer.Position + offset);
                }
                    // Match comments.
                else if ((Tokenizer.Position + 4 <= Tokenizer.Source.Length) && Tokenizer.GetNextTokens(4) == "<!--")
                {
                    int endCharIndex = Tokenizer.Source.IndexOf("->", Tokenizer.Position + 4);
                    if (endCharIndex == -1)
                        endCharIndex = Tokenizer.Source.Length;
                    else
                        endCharIndex += 2;
                    ScannerResult.Add(new Occurrence(Tokenizer.Position, endCharIndex - Tokenizer.Position, xmlCommentNode));
                    Tokenizer.MoveTo(endCharIndex - 1);
                }
                    // Match start identifier.
                else if ((Tokenizer.Position + 2 <= Tokenizer.Source.Length) && Tokenizer.GetNextTokens(2) == "<?")
                {
                    ScannerResult.Add(new Occurrence(Tokenizer.Position, 2, xmlSpecialCharNode));
                    OnMatch(new MatchEventArgs(Tokenizer.Position, 2, null, null, null, MatchType.StartIdentifier));
                    Tokenizer.MoveTo(Tokenizer.Position + 1);
                    identifierEnabled = true;
                }
                    // Match end identifier.
                else if ((Tokenizer.Position + 2 <= Tokenizer.Source.Length) && Tokenizer.GetNextTokens(2) == "?>")
                {
                    ScannerResult.Add(new Occurrence(Tokenizer.Position, 2, xmlSpecialCharNode));
                    OnMatch(new MatchEventArgs(Tokenizer.Position, 2, null, null, null, MatchType.EndIdentifier));
                    Tokenizer.MoveTo(Tokenizer.Position + 1);
                    identifierEnabled = false;
                }
                    // Match attributes.
                else if (identifierEnabled)
                {
                    // TODO: Fix 
                    /*int length = MatchAttributes(1, new NameValueCollection());
					if (length > -1)
						Tokenizer.MoveTo(Tokenizer.Position + length);*/
                }
                    // Match either a starting tag, such as "<asp:Label>", or a single tag, such as "<asp:Label />".
                else if ((Tokenizer.Position + 1 <= Tokenizer.Source.Length) && Tokenizer.GetNextTokens(1) == "<")
                {
                    ScannerResult.Add(new Occurrence(Tokenizer.Position, 1, xmlSpecialCharNode));

                    // Match [special chars/namespace/]tag.
                    int offset = 1;
                    string ns = MatchNamespace(offset);
                    if (ns.Length > 0)
                        offset += ns.Length + 1;

                    string tag = MatchTag(offset);
                    if (tag.Length > 0)
                        offset += tag.Length;

                    // Match attributes.
                    NameValueCollection attributes = new NameValueCollection();
                    if (tag.Length > 0)
                    {
                        int attributesLength = MatchAttributes(offset, attributes);
                        if (attributesLength > -1)
                            offset += attributesLength;
                    }

                    // Match end character(s) of the start/single tag, such as ">" or "/>".
                    int endCharIndex = Tokenizer.Source.IndexOfAny("\0<>".ToCharArray(), Tokenizer.Position + offset);
                    if (endCharIndex > -1)
                    {
                        if (Tokenizer.Source.Substring(endCharIndex, 1).IndexOfAny("\0<".ToCharArray()) == -1)
                        {
                            if (Tokenizer.Source[endCharIndex - 1] == '/')
                            {
                                ScannerResult.Add(new Occurrence(endCharIndex - 1, 2, xmlSpecialCharNode));
                                OnMatch(new MatchEventArgs(Tokenizer.Position, endCharIndex + 1, ns, tag, attributes, MatchType.StandaloneTag));
                            }
                            else
                            {
                                ScannerResult.Add(new Occurrence(endCharIndex, 1, xmlSpecialCharNode));
                                OnMatch(new MatchEventArgs(Tokenizer.Position, endCharIndex + 1, ns, tag, attributes, MatchType.StartTag));
                            }
                        }
                        Tokenizer.MoveTo(endCharIndex - 1);
                    }
                    else
                        Tokenizer.MoveTo(Tokenizer.Position + offset);
                }

                if (!isMatch && Child != null)
                {
                    Child.Scan(token);
                }
            }
        }

        /// <summary>
        /// Matches a namespace.
        /// </summary>
        /// <param name="offset">The offset from which the search can begin.</param>
        /// <returns>The matched namespace.</returns>
        private string MatchNamespace(int offset)
        {
            if ((Tokenizer.Position + offset) < Tokenizer.Source.Length && !char.IsNumber(Tokenizer.Source, Tokenizer.Position + offset))
            {
                for (int i = Tokenizer.Position + offset; i < Tokenizer.Source.Length; i++)
                {
                    if (!char.IsLetter(Tokenizer.Source, i))
                    {
                        if (Tokenizer.Source[i] == ':')
                        {
                            int length = i - (Tokenizer.Position + offset);
                            if (length > 0)
                                ScannerResult.Add(new Occurrence(Tokenizer.Position + offset, length, xmlNamespaceNode));
                            return Tokenizer.Source.Substring(Tokenizer.Position + offset, length);
                        }
                        else
                            return String.Empty;
                    }
                }
            }
            return String.Empty;
        }

        /// <summary>
        /// Matches a tag.
        /// </summary>
        /// <param name="offset">The offset from which the search can begin.</param>
        /// <returns>The matched tag.</returns>
        private string MatchTag(int offset)
        {
            if ((Tokenizer.Position + offset) < Tokenizer.Source.Length && !char.IsNumber(Tokenizer.Source, Tokenizer.Position + offset))
            {
                for (int i = Tokenizer.Position + offset; i < Tokenizer.Source.Length; i++)
                {
                    char ch = Tokenizer.Source[i];
                    if (ch != '.' && !char.IsLetterOrDigit(ch))
                    {
                        int length = i - (Tokenizer.Position + offset);
                        if (length > 0)
                            ScannerResult.Add(new Occurrence(Tokenizer.Position + offset, length, xmlTagNode));
                        return Tokenizer.Source.Substring(Tokenizer.Position + offset, length);
                    }
                }
                ScannerResult.Add(new Occurrence(Tokenizer.Position + offset, Tokenizer.Source.Length - (Tokenizer.Position + offset), xmlTagNode));
                return Tokenizer.Source.Substring(Tokenizer.Position + offset, Tokenizer.Source.Length - (Tokenizer.Position + offset));
            }
            return String.Empty;
        }

        /// <summary>
        /// Matches an attributes.
        /// </summary>
        /// <param name="offset">The offset from which the search can begin.</param>
        /// <param name="attributes">The matched attributes.</param>
        /// <returns>The length of the matched attribute value.</returns>
        private int MatchAttributes(int offset, NameValueCollection attributes)
        {
            // NOTE: This method should be reimplemented. The regexp won't work properly when an attribute without a value is entered, e.g.: <?xml a="b"?>
            int lastCharIndex = Tokenizer.Source.Length;

            // Get end char of the tag (">").
            char stringType = '\0';
            for (int i = Tokenizer.Position + offset; i < Tokenizer.Source.Length; i++)
            {
                if (Tokenizer.Source[i] == '\"' || Tokenizer.Source[i] == '\'')
                {
                    if (stringType == '\0')
                        stringType = Tokenizer.Source[i];
                    else if (stringType == Tokenizer.Source[i])
                        stringType = '\0';
                }
                else if (stringType == '\0' && Tokenizer.Source[i] == '>')
                {
                    lastCharIndex = i;
                    if (Tokenizer.Source[i - 1] == '/')
                        lastCharIndex--;
                    break;
                }
            }

            MatchCollection matches = Regex.Matches(Tokenizer.Source.Substring(Tokenizer.Position + offset, lastCharIndex - (Tokenizer.Position + offset)),
                                                    @"(
					(?'name'\w+) \s* (?'attGroup' = \s*
					(?'value' [^\s""'/\?>]+ | ""[^""]*"" | '[^']*')){0,1}
					\s*
				)*",
                                                    RegexOptions.ExplicitCapture|RegexOptions.IgnorePatternWhitespace|RegexOptions.Compiled);

            int length = -1;
            for (int i = 0; i < matches.Count; i++)
            {
                for (int j = 0; j < matches[i].Groups["name"].Captures.Count; j++)
                {
                    if (matches[i].Groups["name"].Captures[j].Length > 0)
                    {
                        ScannerResult.Add(new Occurrence(Tokenizer.Position + offset + matches[i].Groups["name"].Captures[j].Index, matches[i].Groups["name"].Captures[j].Length, xmlAttributeNameNode));

                        if (j < matches[i].Groups["value"].Captures.Count && matches[i].Groups["value"].Captures[j].Length > 0)
                        {
                            ScannerResult.Add(new Occurrence(Tokenizer.Position + offset + matches[i].Groups["value"].Captures[j].Index, matches[i].Groups["value"].Captures[j].Length, xmlAttributeValueNode));
                            length = matches[i].Groups["value"].Captures[j].Index + matches[i].Groups["value"].Captures[j].Length;
                        }
                        else
                            length = matches[i].Groups["name"].Captures[j].Index + matches[i].Groups["name"].Captures[j].Length;

                        if (matches[i].Groups["name"].Captures[j].Length > 0 && j < matches[i].Groups["value"].Captures.Count && matches[i].Groups["value"].Captures[j].Length > 0)
                            attributes.Add(matches[i].Groups["name"].Captures[j].Value, matches[i].Groups["value"].Captures[j].Value);
                    }
                }
            }

            return length;
        }

        /// <summary>
        /// Raises the <see cref="XmlScanner.Match"/> event.
        /// </summary>
        /// <param name="e">An <see cref="System.EventArgs"/> object that contains the event data.</param>
        protected void OnMatch(MatchEventArgs e)
        {
            if (Match != null)
                Match(this, e);
        }

        /// <summary>
        /// Resets the scanner.
        /// </summary>
        public override void Reset()
        {
            identifierEnabled = false;
        }
    }
}